package oss

import (
	"bytes"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
)

var (
	ErrInvalidHeader         = errors.New("invalid http header for object")
	ErrInvalidDirective      = errors.New("invalid metadata directive")
	ErrInvalidSideEncryption = errors.New("invalid side encryption")
	ErrPermission            = errors.New("invlaid permission")
	ErrEmptyAccessKeyID      = errors.New("empty access key id")
	ErrInvalidParam          = errors.New("invalid param")
)

func (o OssAPI) PutObject(objectName string, filePath string, headers map[string]string) error {
	buffer := new(bytes.Buffer)
	fp, err := os.Open(filePath)
	if err != nil {
		return err
	}

	defer fp.Close()
	io.Copy(buffer, fp)

	return o.PutObjectReader(objectName, buffer, headers)
}

func isValidName(name string, names []string) bool {
	for _, n := range names {
		if n == name {
			return true
		}
	}
	return false
}

func (o OssAPI) PutObjectReader(objectName string, reader io.Reader, headers map[string]string) error {
	supportHeaders := []string{
		"Cache-Control", "Content-Disposition", "Content-Encoding", "Content-MD5",
		"Expires", "x-oss-server-side-encryption", "x-oss-object-acl",
	}

	for key, _ := range headers {
		if key != "" && !isValidName(key, supportHeaders) {
			return ErrInvalidHeader
		}
	}

	h := http.Header{}
	for key, name := range headers {
		h.Set(key, name)
	}

	path := "/" + objectName

	return o.httpPut(path, nil, h, reader, nil)
}

// 当在headers中x-oss-copy-source设置了具体的源信息时，srcBucketName和srcObjectName两值被忽略
func (o OssAPI) CopyObject(dstBucketName, dstObjectName, srcBucketName, srcObjectName string, headers map[string]string) (*CopyObjectResult, error) {
	supportHeaders := []string{
		"x-oss-copy-source", "x-oss-copy-source-if-match", "x-oss-copy-source-if-unmodified-since",
		"x-oss-copy-source-if-modified-since", "x-oss-metadata-directive", "x-oss-server-side-encryption",
		"x-oss-object-acl",
	}
	for key, _ := range headers {
		if key != "" && !isValidName(key, supportHeaders) {
			return nil, ErrInvalidHeader
		}
	}

	if directive, ok := headers["x-oss-metadata-directive"]; ok {
		if directive != "COPY" && directive != "REPLACE" {
			return nil, ErrInvalidDirective
		}
	}

	if encryption, ok := headers["x-oss-server-side-encryption"]; ok {
		if encryption != "AES256" {
			return nil, ErrInvalidSideEncryption
		}
	}

	if acl, ok := headers["x-oss-object-acl"]; ok {
		switch acl {
		case "public-read", "private", "public-read-write":
		default:
			return nil, ErrPermission
		}
	}

	h := http.Header{}
	for key, name := range headers {
		h.Set(key, name)
	}

	if _, ok := headers["x-oss-copy-source"]; !ok {
		h.Set("x-oss-copy-source", "/"+srcBucketName+"/"+srcObjectName)
	}

	result := CopyObjectResult{}

	err := o.httpPut("/"+dstObjectName, nil, h, nil, &result)
	if err != nil {
		return nil, err
	}

	return &result, nil
}

func (o OssAPI) GetObject(objectName string, headers map[string]string) (io.Reader, error) {
	supportHeaders := []string{
		"response-content-type", "response-content-language", "response-expires", "response-cache-control",
		"response-content-disposition", "response-content-encoding", "Range", "If-Modified-Since",
		"If-Unmodified-Since", "If-Match", "If-None-Match",
	}

	for key, _ := range headers {
		if !isValidName(key, supportHeaders) {
			return nil, ErrInvalidHeader
		}
	}

	h := http.Header{}
	for key, name := range headers {
		h.Set(key, name)
	}

	buff := bytes.Buffer{}
	err := o.httpPut("/"+objectName, nil, h, nil, &buff)
	if err != nil {
		return nil, err
	}

	reader := bytes.NewReader(buff.Bytes())
	return reader, nil
}

func (o OssAPI) AppendObject(objectName string, position int, filePath string, headers map[string]string) error {
	buffer := new(bytes.Buffer)
	fp, err := os.Open(filePath)
	if err != nil {
		return err
	}

	defer fp.Close()
	io.Copy(buffer, fp)

	return o.AppendObjectReader(objectName, position, buffer, headers)
}

func (o OssAPI) AppendObjectReader(objectName string, position int, reader io.Reader, headers map[string]string) error {
	supportHeaders := []string{
		"Cache-Control", "Content-Disposition", "Content-Encoding", "Content-MD5",
		"Expires", "x-oss-server-side-encryption", "x-oss-object-acl",
	}

	for key, _ := range headers {
		if key != "" && !isValidName(key, supportHeaders) {
			return ErrInvalidHeader
		}
	}

	h := http.Header{}
	for key, name := range headers {
		h.Set(key, name)
	}

	path := "/" + objectName + "?append&position=" + fmt.Sprintf("%d", position)
	return o.httpPost(path, nil, h, reader, nil)
}

func (o OssAPI) DeleteObject(objectName string) error {
	path := "/" + objectName
	return o.httpDelete(path, nil, nil, nil)
}

func (o OssAPI) DeleteMultipleObjects(d Delete, encodingType string) (*DeleteResult, error) {
	request, err := xml.Marshal(&d)
	if err != nil {
		return nil, err
	}

	request = []byte(`<?xml version="1.0" encoding="UTF-8"?>` + string(request))
	path := "/?delete"

	v := url.Values{}
	if encodingType != "" {
		v.Set("encoding-type", encodingType)
	}

	result := DeleteResult{}
	err = o.httpPost(path, v, nil, request, &result)
	if err != nil {
		return nil, err
	}

	return &result, nil
}

func (o OssAPI) HeadObject(objectName string, headers map[string]string) error {
	supportHeaders := []string{
		"If-Modified-Since", "If-Unmodified-Since", "If-Match", "If-None-Match",
	}

	for _, name := range supportHeaders {
		if isValidName(name, supportHeaders) {
			return ErrInvalidHeader
		}
	}

	h := http.Header{}
	for key, name := range headers {
		h.Set(key, name)
	}

	path := "/" + objectName
	return o.httpHead(path, h)
}

func (o OssAPI) GetObjectMeta(objectName string) error {
	path := "/" + objectName
	v := url.Values{}
	v.Set("objectMeta", "")

	return o.httpGet(path, v, nil, nil)
}

func (o OssAPI) PutObjectAcl(objectName string, permission string) error {
	if permission == "" {
		permission = "default"
	}

	supportPermissions := []string{
		"private", "public-read", "public-read-write", "default",
	}

	if !isValidName(permission, supportPermissions) {
		return ErrPermission
	}

	path := "/" + objectName + "?acl"

	h := http.Header{}
	h.Set("x-oss-object-acl", permission)

	return o.httpPut(path, nil, h, nil, nil)
}

func (o OssAPI) GetObjectAcl(objectName string) (*AccessControlPolicy, error) {
	policy := AccessControlPolicy{}

	path := "/" + objectName + "?acl"
	err := o.httpGet(path, nil, nil, &policy)
	if err != nil {
		return nil, err
	}

	return &policy, nil
}

type OssPostObject struct {
	OSSAccessKeyID       string
	Policy               string
	Signature            string
	Key                  string
	SuccessRedirect      string
	SuccessStatus        int
	OssMeta              map[string]string
	Header               map[string]string
	ServerSideEncryption string
	ObjectAcl            string
}

func (o OssAPI) PostObject(ossObject OssPostObject, files map[string]string, callback *OssCallback) (*MultipartPostResponse, error) {
	if ossObject.Policy == "" && ossObject.Signature == "" && ossObject.OSSAccessKeyID == "" {
		return nil, ErrEmptyAccessKeyID
	}

	supportHeaders := []string{
		"Cache-Control", "Content-Type", "Content-Disposition", "Content-Encoding", "Expires",
	}

	for key, _ := range ossObject.Header {
		if key != "" && !isValidName(key, supportHeaders) {
			return nil, ErrInvalidHeader
		}
	}

	for key, _ := range ossObject.OssMeta {
		if !strings.HasPrefix(key, "x-oss-meta") {
			return nil, ErrInvalidParam
		}
	}

	path := "/"

	v := url.Values{}
	if ossObject.Key != "" {
		v.Set("key", ossObject.Key)
	}
	if ossObject.SuccessRedirect != "" {
		v.Set("success_action_redirect", ossObject.SuccessRedirect)
	}
	if ossObject.SuccessStatus != 0 {
		v.Set("success_action_status", fmt.Sprintf("%d", ossObject.SuccessStatus))
	}

	for key, name := range ossObject.Header {
		v.Set(key, name)
	}

	for key, name := range ossObject.OssMeta {
		v.Set(key, name)
	}

	if ossObject.OSSAccessKeyID != "" {
		v.Set("OSSAccessKeyId", ossObject.OSSAccessKeyID)
	}
	if ossObject.Policy != "" {
		v.Set("policy", ossObject.Policy)
	}
	if ossObject.Signature != "" {
		v.Set("Signature", ossObject.Signature)
	}
	if ossObject.ServerSideEncryption != "" {
		v.Set("x-oss-server-side-encryption", ossObject.ServerSideEncryption)
	}
	if ossObject.ObjectAcl != "" {
		v.Set("x-oss-object-acl", ossObject.ObjectAcl)
	}

	if callback != nil {
		if callback.Url != "" {
			v.Set("callback", url.QueryEscape(callback.Url))
		}

		for key, value := range callback.Fields {
			v.Set(key, value)
		}

	}

	resp := MultipartPostResponse{}
	err := o.httpPostMultipart(path, v, nil, files, &resp)
	if err != nil {
		return nil, err
	}

	return &resp, nil
}
